/*
# pymoz, Python extension for the Mozilla Toolkit
# Copyright (C) 2012 Malek Hadj-Ali
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3
# as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/


#include "pyException.h"
#include "pyRuntime.h"
#include "pyWrappers.h"

#include "nsServiceManagerUtils.h"
#include "nsComponentManagerUtils.h"
#include "nsDirectoryServiceDefs.h"
#include "nsDirectoryServiceUtils.h"

#include "nsDOMJSUtils.h"

#include "nsIObserverService.h"
#include "nsIChromeRegistry.h"
#include "nsIJSRuntimeService.h"


static const char chrome_scheme[] = "chrome";


/*******************************************************************************
* JSHolder
*******************************************************************************/

JSHolder::JSHolder(JSObject *jsobj)
    : mJSObj(jsobj)
{
}


JSHolder::~JSHolder()
{
    if (mJSObj) {
        pyRuntime::DropJSObject(&mJSObj);
    }
}


bool
JSHolder::Hold()
{
    if (mJSObj) {
        if (pyRuntime::HoldJSObject(&mJSObj)) {
            return true;
        }
        else {
            mJSObj = NULL;
        }
    }
    return false;
}


/*******************************************************************************
* JSAutoContextPusher
*******************************************************************************/

JSAutoContextPusher::JSAutoContextPusher(JSContext *cx)
{
    pyRuntime::sJSContextStack->Push(cx);
}


JSAutoContextPusher::~JSAutoContextPusher()
{
    JSContext *cx = NULL;
    pyRuntime::sJSContextStack->Pop(&cx);

    JSContext *currentCx = NULL;
    pyRuntime::sJSContextStack->Peek(&currentCx);

    if (!currentCx) {
        nsIScriptContext *scx = GetScriptContextFromJSContext(cx);
        if (scx) {
            scx->ScriptEvaluated(true);
        }
    }
}


/*******************************************************************************
* pyRuntime
*******************************************************************************/

PyObject *pyRuntime::sError = NULL;
PyObject *pyRuntime::sJSError = NULL;
PyObject *pyRuntime::sXPCOMError = NULL;
PyObject *pyRuntime::sJSWarning = NULL;
nsIThreadJSContextStack *pyRuntime::sJSContextStack;
PyObjectCache *pyRuntime::sPyCache = NULL;
JSObjectCache *pyRuntime::sJSCache = NULL;

nsIXPConnect *pyRuntime::sXPConnect;
nsresult pyRuntime::sResult = NS_ERROR_FAILURE;
JSRuntime *pyRuntime::sJSRuntime = NULL;

pyRuntime *pyRuntime::sPyRuntime = NULL;


NS_IMPL_ISUPPORTS1(pyRuntime, pyIRuntime)


/* interface pyIRuntime ----------------------------------------------------- */

NS_IMETHODIMP
pyRuntime::Run(const nsACString& aSpec, const char *aOriginCharset,
               nsIURI *aBaseURI, JSContext *cx)
{
    JSAutoContextPusher acp(cx);
    sResult = NS_ERROR_FAILURE;
    char *path = NULL;
    nsCOMPtr<nsIFile> aFile = GetFile(aSpec, aOriginCharset, aBaseURI, &path);
    if (aFile) {
        FILE *fp;
        if (NS_FAILED(aFile->OpenANSIFileDesc("r", &fp))) {
            PyErr_SetFromErrnoWithFilename(PyExc_IOError, path);
        }
        else {
            sResult = NS_OK;
            if (!PyRun_SimpleFile(fp, path) && !UpdateWindow()) {
                sResult = NS_ERROR_FAILURE;
            }
            fclose(fp);
        }
        NS_Free(path);
    }
    if (NS_FAILED(sResult)) {
        ReportError();
    }
    return sResult;
}


/* interface nsIObserver ---------------------------------------------------- */

NS_IMETHODIMP
pyRuntime::Observe(nsISupports *aSubject, const char *aTopic,
                   const PRUnichar *aData)
{
    if (!strcmp(aTopic, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID)) {
        nsCOMPtr<nsIObserverService> obsSvc =
            do_GetService("@mozilla.org/observer-service;1");
        if (obsSvc) {
            obsSvc->RemoveObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID);
        }
        Finalize(false);
    }
    return NS_OK;
}


/* pyRuntime - Initialization/Finalization ---------------------------------- */

pyRuntime::pyRuntime()
{
}


pyRuntime::~pyRuntime()
{
    Finalize(true);
}


pyRuntime *
pyRuntime::GetService()
{
    if (!sPyRuntime) {
        sPyRuntime = new pyRuntime();
        if (sPyRuntime && !sPyRuntime->Initialize()) {
            delete sPyRuntime;
            sPyRuntime = NULL;
        }
    }
    NS_IF_ADDREF(sPyRuntime);
    return sPyRuntime;
}


bool
pyRuntime::InitPython_Phase1()
{
    _Py_IDENTIFIER(excepthook);

    // init exceptions
    sError = PyErr_NewException("pymoz.Error", NULL, NULL);
    sJSError = PyErr_NewException("pymoz.JSError", sError, NULL);
    sXPCOMError = PyErr_NewException("pymoz.XPCOMError", sError, NULL);
    sJSWarning = PyErr_NewException("pymoz.JSWarning", PyExc_Warning, NULL);
    if (!sError || !sJSError || !sXPCOMError || !sJSWarning) {
        return false;
    }

    // set excepthook
    PyObject *excepthook = PyModule_GetAttrId("pymoz", &PyId_excepthook);
    if (!excepthook) {
        return false;
    }
    bool result = !!!PySys_SetObject(PyId_excepthook.string, excepthook);
    Py_DECREF(excepthook);
    return result;
}


bool
pyRuntime::InitPython_Phase2()
{
    _Py_IDENTIFIER(showwarning);
    _Py_IDENTIFIER(__showwarning__);

    // set showwarning
    PyObject *__showwarning__ = PyModule_GetAttrId("warnings", &PyId_showwarning);
    PyObject *showwarning = PyModule_GetAttrId("pymoz", &PyId_showwarning);
    if (!(__showwarning__ && showwarning)) {
        Py_XDECREF(showwarning);
        Py_XDECREF(__showwarning__);
        return false;
    }
    bool result = !!!PyModule_SetAttrId("warnings", &PyId___showwarning__,
                                        __showwarning__);
    if (result) {
        result = !!!PyModule_SetAttrId("warnings", &PyId_showwarning,
                                       showwarning);
    }
    Py_DECREF(showwarning);
    Py_DECREF(__showwarning__);

    if (result) {
        // set sys path
        nsCOMPtr<nsIFile> nsCurPath;
        nsresult rv = NS_GetSpecialDirectory(NS_OS_CURRENT_PROCESS_DIR,
                                             getter_AddRefs(nsCurPath));
        PY_ENSURE_SUCCESS(rv, false, sXPCOMError, "Failed to get current path");

        rv = nsCurPath->AppendNative(NS_LITERAL_CSTRING("site-packages"));
        PY_ENSURE_SUCCESS(rv, false, sXPCOMError,
                          "Failed to append 'site-packages' to current path");

        nsCString nsLibPath;
        rv = nsCurPath->GetNativePath(nsLibPath);
        PY_ENSURE_SUCCESS(rv, false, sXPCOMError,
                          "Failed to get updated modules search path");

        PyObject *pysyspath = PySys_GetObject("path"); // borrowed
        if (!pysyspath) {
            PyErr_SetString(sError, "Failed to get sys.path");
            return false;
        }

        PyObject *pylibpath = PyUnicode_DecodeFSDefault(nsLibPath.get());
        if (!pylibpath) {
            return false;
        }
        result = !PyList_Insert(pysyspath, 0, pylibpath);
        Py_DECREF(pylibpath);
    }

    return result;
}


bool
pyRuntime::InitRuntime()
{
    nsresult rv = NS_ERROR_FAILURE;

    //PY_ENSURE_SUCCESS(rv, false, sXPCOMError, "Test");

    // JSRuntime
    nsCOMPtr<nsIJSRuntimeService> jsrSvc =
        do_GetService("@mozilla.org/js/xpc/RuntimeService;1", &rv);
    PY_ENSURE_SUCCESS(rv, false, sXPCOMError,
                      "Failed to get nsIJSRuntimeService");
    rv = jsrSvc->GetRuntime(&sJSRuntime);
    PY_ENSURE_SUCCESS(rv, false, sXPCOMError, "Failed to get JSRuntime");

    // JSContextStack
    //sJSContextStack = do_GetService("@mozilla.org/js/xpc/ContextStack;1", &rv);
    rv = CallGetService("@mozilla.org/js/xpc/ContextStack;1", &sJSContextStack);
    PY_ENSURE_SUCCESS(rv, false, sXPCOMError, "Failed to get ContextStack");

    // caches
    sPyCache = new PyObjectCache();
    sPyCache->Init();

    sJSCache = new JSObjectCache();
    sJSCache->Init();

    // observing
    nsCOMPtr<nsIObserverService> obsSvc =
        do_GetService("@mozilla.org/observer-service;1", &rv);
    PY_ENSURE_SUCCESS(rv, false, sXPCOMError,
                      "Failed to get nsIObserverService");
    rv = obsSvc->AddObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID, false);
    PY_ENSURE_SUCCESS(rv, false, sXPCOMError, "Failed to add observer");

    return true;
}


bool
pyRuntime::InitWrappers()
{
    return (PyJSObject_InitTypes() && JSPyObject_InitPrototype());
}


bool
pyRuntime::Initialize()
{
    // XPConnect first
    //sXPConnect = do_GetService(nsIXPConnect::GetCID(), &sResult);
    sResult = CallGetService(nsIXPConnect::GetCID(), &sXPConnect);
    NS_ENSURE_SUCCESS(sResult, false);
    sXPConnect->SetReportAllJSExceptions(true);

    // init Python
    if (!InitPython_Phase1()) {
        return false;
    }

    /* --- past this point report errors in Python --- */

    if (!InitPython_Phase2() || !InitRuntime() || !InitWrappers()) {
        sResult = NS_ERROR_XPC_GS_RETURNED_FAILURE;
        ReportError();
        return false;
    }
    return true;
}


static PLDHashOperator
PyCache_Clear(JSObject *jsobj, PyObject *pyobj, void *closure)
{
    Py_DECREF(pyobj);
    return PL_DHASH_NEXT;
}


void
pyRuntime::Finalize(bool final)
{
    if (Py_IsInitialized()) {
        if (PyErr_Occurred()) {
            PyErr_Print();
        }
        //cleanup globals
        PyObject *globals = Py_GetGlobals(); // borrowed
        if (globals) {
            PyObject *key, *value;
            Py_ssize_t pos = 0;
            while (PyDict_Next(globals, &pos, &key, &value)) {
                if (PyJSObject_Check(value)) {
                    if (PyDict_SetItem(globals, key, Py_None)) {
                        PyErr_Print();
                    }
                }
            }
        }

        //clear/delete caches
        if (sJSCache) {
            if (sJSCache->IsInitialized()) {
                sJSCache->Clear();
            }
            delete sJSCache;
            sJSCache = NULL;
        }
        if (sPyCache) {
            if (sPyCache->IsInitialized()) {
                sPyCache->EnumerateRead(PyCache_Clear, NULL);
                sPyCache->Clear();
            }
            delete sPyCache;
            sPyCache = NULL;
        }
        NS_IF_RELEASE(sJSContextStack);
        NS_IF_RELEASE(sXPConnect);
        if (final) {
            sJSRuntime = NULL;
            Py_CLEAR(sJSWarning);
            Py_CLEAR(sXPCOMError);
            Py_CLEAR(sJSError);
            Py_CLEAR(sError);
        }
    }
}


/* pyRuntime - helpers ------------------------------------------------------ */

JSContext *
pyRuntime::GetJSContext()
{
    JSContext *cx = NULL;

    sJSContextStack->Peek(&cx);
    if (!cx) {
        cx = sJSContextStack->GetSafeJSContext();
    }
    if (!cx) {
        PyErr_SetString(sXPCOMError, "Failed to get JSContext");
    }
    return cx;
}


PyObject *
pyRuntime::GetWindow()
{
    JSContext *cx = GetJSContext();
    if (!cx) {
        return NULL;
    }
    return PyJSObject_NewOrUsed(cx, JS_GetGlobalForScopeChain(cx));
}


bool
pyRuntime::SetException(PyObject *aExcValue)
{
    nsCOMPtr<nsIException> aExc =
        pyException::Create(aExcValue,
                            NS_FAILED(sResult) ? sResult : NS_ERROR_FAILURE);
    if (!aExc) {
        return false;
    }
    nsresult rv;
    if (NS_FAILED(sResult)) {
        rv = sXPConnect->SetPendingException(aExc);
        PY_ENSURE_SUCCESS(rv, false, sXPCOMError,
                          "Failed to set pending exception");
    }
    else {
        JSContext *cx = GetJSContext();
        if (!cx) {
            return false;
        }
        nsCOMPtr<nsIXPConnectJSObjectHolder> aHolder;
        rv = sXPConnect->WrapNative(cx, JS_GetGlobalForScopeChain(cx), aExc,
                                    NS_GET_IID(nsIException),
                                    getter_AddRefs(aHolder));
        PY_ENSURE_SUCCESS(rv, false, sXPCOMError, "Failed to wrap nsIException");
        JSObject* jsobj;
        rv = aHolder->GetJSObject(&jsobj);
        PY_ENSURE_SUCCESS(rv, false, sXPCOMError,
                          "Failed to get JSObject from Holder");
        JS_SetPendingException(cx, OBJECT_TO_JSVAL(jsobj));
    }
    return true;
}


bool
pyRuntime::ReportWarning(PyObject *aMessage, PyObject *aCategory,
                         PyObject *aFilename, PyObject *aLineno,
                         PyObject *aLine)
{
    nsCOMPtr<nsIScriptError> aWarn =
        pyWarning::Create(aMessage, aCategory, aFilename, aLineno, aLine);
    if (!aWarn) {
        return false;
    }
    nsCString warning;
    nsresult rv = aWarn->ToString(warning);
    PY_ENSURE_SUCCESS(rv, false, sXPCOMError, "Failed to get warning message");
    JSContext *cx = GetJSContext();
    if (!cx) {
        return false;
    }
    JS_ReportWarning(cx, warning.get());
    return true;
}


void
pyRuntime::ReportError()
{
    bool need_tb = false;
    _Py_IDENTIFIER(print_file_and_line);
    PyObject *exctype, *excvalue, *exctraceback;

    PyErr_Fetch(&exctype, &excvalue, &exctraceback);
    PyErr_NormalizeException(&exctype, &excvalue, &exctraceback);
    if (!_PyObject_HasAttrId(excvalue, &PyId_print_file_and_line) &&
        !(exctraceback && PyTraceBack_Check(exctraceback))) {
        need_tb = true;
    }
    PyErr_Restore(exctype, excvalue, exctraceback);

    if (need_tb) {
        nsCOMPtr<nsIStackFrame> aLocation;
        sXPConnect->GetCurrentJSStack(getter_AddRefs(aLocation));
        if (aLocation) {
            char *filename = NULL, *name = NULL;
            int lineno = 0;
            aLocation->GetFilename(&filename);
            aLocation->GetName(&name);
            aLocation->GetLineNumber(&lineno);
            if (filename && name) {
                nsCString aPath;
                if (ConvertChromeURL(filename, aPath)) {
                    PyTraceback_FromLocation(aPath.get(), name, lineno);
                }
            }
            if (name) {
                NS_Free(name);
            }
            if (filename) {
                NS_Free(filename);
            }
        }
    }

    PyErr_Print();
}


bool
pyRuntime::HoldJSObject(JSObject **jsobj)
{
    bool result = js_AddGCThingRootRT(sJSRuntime, (void **)jsobj, "pymoz");
    if (!result) {
        PyErr_NoMemory();
    }
    return result;
}


void
pyRuntime::DropJSObject(JSObject **jsobj)
{
    js_RemoveRoot(sJSRuntime, (void *)jsobj);
}


bool
pyRuntime::ConvertChromeURL(const char *aSpec, nsACString& aPath)
{

    nsresult rv = NS_ERROR_FAILURE;
    bool isChrome = false;
    nsCOMPtr<nsIURI> aURL;

    nsCOMPtr<nsIURI> aURI;
    rv = NS_NewURI(getter_AddRefs(aURI), aSpec);
    PY_ENSURE_SUCCESS(rv, false, sXPCOMError, "Failed to create URL");

    rv = aURI->SchemeIs(chrome_scheme, &isChrome);
    PY_ENSURE_SUCCESS(rv, false, sXPCOMError, "Failed to check URL's scheme");
    if (isChrome) {
        nsCOMPtr<nsIChromeRegistry> crSvc =
            do_GetService(NS_CHROMEREGISTRY_CONTRACTID, &rv);
        PY_ENSURE_SUCCESS(rv, false, sXPCOMError,
                          "Failed to get nsIChromeRegistry");

        rv = crSvc->ConvertChromeURL(aURI, getter_AddRefs(aURL));
        PY_ENSURE_SUCCESS(rv, false, sXPCOMError,
                          "Failed to convert chrome URL to file URL");
    }
    else {
        rv = aURI->Clone(getter_AddRefs(aURL));
        PY_ENSURE_SUCCESS(rv, false, sXPCOMError, "Failed to clone URL");
    }

    nsCOMPtr<nsIFileURL> aFileURL = do_QueryInterface(aURL, &rv);
    if (NS_SUCCEEDED(rv)) {
        nsCOMPtr<nsIFile> aFile;
        rv = aFileURL->GetFile(getter_AddRefs(aFile));
        PY_ENSURE_SUCCESS(rv, false, sXPCOMError, "Failed to get file");

        rv = aFile->GetNativePath(aPath);
        PY_ENSURE_SUCCESS(rv, false, sXPCOMError, "Failed to get native path");
    }
    else {
        aPath.Assign(aSpec);
    }

    return true;
}


/* pyRuntime - utils -------------------------------------------------------- */

already_AddRefed<nsIFile>
pyRuntime::GetFile(const nsACString& aSpec, const char *aOriginCharset,
                   nsIURI *aBaseURI, char **path)
{
    nsresult rv = NS_ERROR_FAILURE;
    bool isChrome = false;

    //PY_ENSURE_SUCCESS(rv, NULL, sXPCOMError, "Test");

    nsCOMPtr<nsIURI> aURI;
    rv = NS_NewURI(getter_AddRefs(aURI), aSpec, aOriginCharset, aBaseURI);
    PY_ENSURE_SUCCESS(rv, NULL, sXPCOMError, "Failed to create URL");

    rv = aURI->SchemeIs(chrome_scheme, &isChrome);
    PY_ENSURE_SUCCESS(rv, NULL, sXPCOMError, "Failed to check URL's scheme");

    if (!isChrome) {
        PyErr_SetString(sError, "python 'src' attribute must be a chrome URL");
        return NULL;
    }

    nsCOMPtr<nsIChromeRegistry> crSvc =
        do_GetService(NS_CHROMEREGISTRY_CONTRACTID, &rv);
    PY_ENSURE_SUCCESS(rv, NULL, sXPCOMError, "Failed to get nsIChromeRegistry");

    nsCOMPtr<nsIURI> aURL;
    rv = crSvc->ConvertChromeURL(aURI, getter_AddRefs(aURL));
    PY_ENSURE_SUCCESS(rv, NULL, sXPCOMError,
                      "Failed to convert chrome URL to file URL");

    nsCOMPtr<nsIFileURL> aFileURL = do_QueryInterface(aURL, &rv);
    PY_ENSURE_SUCCESS(rv, NULL, sXPCOMError,
                      "Failed to QueryInterface nsIURI to nsIFileURL");

    nsCOMPtr<nsIFile> aFile;
    rv = aFileURL->GetFile(getter_AddRefs(aFile));
    PY_ENSURE_SUCCESS(rv, NULL, sXPCOMError, "Failed to get file");

    nsCString aPath;
    rv = aFile->GetNativePath(aPath);
    PY_ENSURE_SUCCESS(rv, NULL, sXPCOMError, "Failed to get native path");

    *path = ToNewCString(aPath);
    if (!(*path)) {
        PyErr_NoMemory();
        return NULL;
    }

    return aFile.forget();
}


bool
pyRuntime::UpdateWindow()
{
    PyObject *globals = Py_GetGlobals(); // borrowed
    PyObject *window = GetWindow();
    PyObject *list = NULL;

    if (!(globals && window)) {
        goto fail;
    }
    list = PyDict_GetItemString(globals, "__window__"); // borrowed
    if (list) {
        if (!PyList_CheckExact(list)) {
            PyErr_SetString(PyExc_TypeError, "__window__ must be a list");
            goto fail;
        }
        Py_ssize_t size = PyList_GET_SIZE(list);
        PyObject *name, *attr;
        for (Py_ssize_t i = 0; i < size; i++) {
            name = PyList_GET_ITEM(list, i); // borrowed
            if (!PyUnicode_Check(name)) {
                PyErr_SetString(PyExc_TypeError,
                                "__window__'s items must be string objects");
                goto fail;
            }
            attr = PyDict_GetItemWithError(globals, name); // borrowed
            if (!attr) {
                if (!PyErr_Occurred()) {
                    PyErr_Format(PyExc_NameError,
                                 "global name '%U' is not defined", name);
                }
                goto fail;
            }
            if (PyObject_SetAttr(window, name, attr)) {
                goto fail;
            }
        }
    }
    Py_DECREF(window);
    return true;

fail:
    Py_XDECREF(window);
    return false;
}
